package org.lab.tracecash.web.rest;

import java.io.Serializable;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.ws.rs.NotFoundException;

import org.apache.commons.lang.StringUtils;
import org.lab.tracecash.common.jpa.FiqlParser;
import org.lab.tracecash.common.model.SearchOrder;
import org.lab.tracecash.common.model.SearchParams;
import org.lab.tracecash.common.model.SearchResults;

public abstract class AbstractRestEntityService<I> extends AbstractRestService {

	protected static final int DEFAULT_MAX_RESULTS = 20;

	@Inject
	protected FiqlParser fiqlParser;
	@Inject
	protected Provider<EntityManager> entityManagerProvider;

	protected abstract Class<I> getEntityClass();

	protected I findById(Serializable primaryKey) {
		EntityManager entityManager = entityManagerProvider.get();
		I entity = entityManager.find(getEntityClass(), primaryKey);
		if (entity == null) {
			throw new NotFoundException();
		}
		return entity;
	}

	protected SearchResults<I> find(SearchParams params) {
		Class<I> entityClass = getEntityClass();
		EntityManager entityManager = entityManagerProvider.get();
		CriteriaBuilder builder = entityManager.getCriteriaBuilder();
		CriteriaQuery<I> criteria = builder.createQuery(entityClass);
		Root<I> root = criteria.from(entityClass);
		Predicate predicate = StringUtils.isBlank(params.getQueryString()) ? builder.conjunction() : fiqlParser.parse(params.getQueryString(), builder, root).build();
		criteria.where(predicate);
		// Ordenación de los resultados
		if (params.getOrderColumn() != null) {
			Path<Object> orderColumnPath = root.get(params.getOrderColumn());
			Order order = (params.getOrder() == null || params.getOrder() == SearchOrder.ASC) ? builder.asc(orderColumnPath) : builder.desc(orderColumnPath);
			criteria.orderBy(order);
		}
		buildOrderCriteria(criteria, builder, root);
		TypedQuery<I> query = entityManager.createQuery(criteria);
		configureQueryRange(query, params);
		SearchResults<I> searchResults = new SearchResults<I>();
		searchResults.setResults(query.getResultList());
		// Numero de resultados
		CriteriaQuery<Long> criteriaCount = builder.createQuery(Long.class);
		criteriaCount.select(builder.count(root));
		criteriaCount.where(predicate);
		Long totalItems = entityManager.createQuery(criteriaCount).getSingleResult();
		Long totalPages = (totalItems + (totalItems % params.getPageSize())) / params.getPageSize();
		searchResults.setTotalItems(totalItems);
		searchResults.setTotalPages(totalPages);
		searchResults.setCurrentPage(params.getCurrentPage());
		searchResults.setPageSize(params.getPageSize());
		return searchResults;
	}

	/**
	 * Podemos sobreescribir este metodo para establecer la ordenacion de resultados en nuestras consultas.
	 * 
	 * @param criteria
	 * @param builder
	 * @param root
	 */
	protected void buildOrderCriteria(CriteriaQuery<I> criteria, CriteriaBuilder builder, Root<I> root) {
	}

	/**
	 * Podemos sobreescribir este metodo para establecer el numero de resultados por defecto.
	 * 
	 * @return
	 */
	protected Integer getDefaultsItemsPerPage() {
		return 10;
	}

	private void configureQueryRange(Query query, SearchParams params) {
		if (params.getCurrentPage() == null || params.getCurrentPage() < 1L) {
			params.setCurrentPage(1L);
		}
		if (params.getPageSize() == null || params.getPageSize() < 1) {
			params.setPageSize(getDefaultsItemsPerPage());
		}
		query.setMaxResults(params.getPageSize());
		query.setFirstResult((int) (params.getPageSize() * (params.getCurrentPage() - 1)));
	}

}
